package com.hup.vd.service.m3u8;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.hup.utils.commons.DateBuilder;
import com.hup.utils.commons.exception.AppCommonException;
import com.hup.utils.commons.start.SpringContext;
import com.hup.vd.model.config.AppConfig;
import com.hup.vd.model.config.DownloadConfig;
import com.hup.vd.model.config.TaskInfo;
import com.hup.vd.model.selenium.ChromeMessageMatcherConfig;
import com.hup.vd.service.CancelState;
import com.hup.vd.util.CommonUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeDriverService;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.logging.LogEntries;
import org.openqa.selenium.logging.LogEntry;
import org.openqa.selenium.logging.LogType;
import org.openqa.selenium.logging.LoggingPreferences;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;

/**
 * 用于抓取html中的m3u8请求
 *
 * @author hugan
 * @date 2022/8/9
 */
@Slf4j
@Component
public class HtmlUrlHandler {

    /**
     * 导出chrome日志的目录
     */
    private static final String LOG_HOME = "./logs";

    private final AppConfig appConfig;
    private final DownloadConfig downloadConfig;
    private final ReentrantLock lock;
    private final ChromeMessageMatcher messageMatcher;

    private boolean inited;
    /**
     * 一旦启动,则只在关闭程序时才会关闭浏览器
     */
    private WebDriver browser;

    public HtmlUrlHandler(AppConfig appConfig) {
        this.appConfig = appConfig;
        downloadConfig = appConfig.getDownloadConfig();
        lock = new ReentrantLock(true);
        messageMatcher = new ChromeMessageMatcher(new ChromeMessageMatcherConfig().loadFromFile());
    }

    private void init() {
        if (inited) return;
        AppConfig appConfig = SpringContext.getBean(AppConfig.class);
        if (!StringUtils.hasText(appConfig.getChromeDriverPath())) {
            throw new AppCommonException("未配置ChromeDriver");
        }
        System.setProperty(ChromeDriverService.CHROME_DRIVER_EXE_PROPERTY, appConfig.getChromeDriverPath());
        inited = true;
    }

    /**
     * 打开浏览器
     */
    private WebDriver openBrowser() {
        log.info("浏览器.正在启动");
        ChromeOptions options = new ChromeOptions();
        options.setHeadless(true);
        LoggingPreferences logPrefs = new LoggingPreferences();
        logPrefs.enable(LogType.PERFORMANCE, Level.ALL);
        options.setCapability("goog:loggingPrefs", logPrefs);

        HashMap<String, Object> perfLoggingPrefs = new HashMap<>();
        perfLoggingPrefs.put("enableNetwork", true);
        options.setExperimentalOption("perfLoggingPrefs", perfLoggingPrefs);
        ChromeDriver res = new ChromeDriver(options);
        log.info("浏览器.启动成功");
        return res;
    }

    /**
     * 获取已启动的浏览器
     * 处理页签句柄或浏览器关闭的情况
     * .浏览器关闭:getWindowHandles异常
     */
    private void getBrowser(CancelState cancelState) {
        if (browser == null) {
            browser = openBrowser();
            return;
        }

        try {
            browser.getWindowHandles();
            return;//都正常:复用已有浏览器
        } catch (WebDriverException e) {
            //浏览器进程被用户结束
            log.warn("浏览器.进程不可用={}", e.getMessage());
        }
        if (cancelState.isCanceled()) return;
        try {
            log.info("浏览器.尝试关闭");
            browser.quit();//关闭原有的chromeDriver.exe; 这也是一个耗时操作:大概在1秒内
        } catch (Exception e) {
            //ChromeDriver进程被用户结束
            log.error("浏览器.关闭错误={}", e.getMessage());
        }
        if (cancelState.isCanceled()) return;
        browser = openBrowser();
    }

    /**
     * @param cancelState 用于判断任务是否被取消
     * @return nullable-任务被取消
     */
    public HtmlInfo getM3u8Url(String url, CancelState cancelState, TaskInfo task) {
        if (url.endsWith(".m3u8")) throw new IllegalArgumentException("代码异常,url异常");
        init();

        try {
            //下面代码,每个[耗时,解锁等]操作后,都要判断cancelState
            lock.lock();
            if (cancelState.isCanceled()) return null;
            getBrowser(cancelState);
            if (cancelState.isCanceled()) return null;

            //重试条件
            int tryCount = 0;
            long st = System.currentTimeMillis();
            while (true) {
                if (cancelState.isCanceled()) return null;

                tryCount++;
                if (tryCount > downloadConfig.m3u8RetryCount) {
                    log.error("获取m3u8链接失败, {}", url);
                    throw new AppCommonException("抓取m3u8链接失败:重试次数超过" + downloadConfig.m3u8RetryCount + "次");
                }
                if ((System.currentTimeMillis() - st) > downloadConfig.m3u8Timeout) {
                    log.error("获取m3u8链接失败, {}", url);
                    throw new AppCommonException("抓取m3u8链接失败:请求超时");
                }

                log.info("页面.开始加载={}", url);
                try {
                    browser.manage().timeouts().pageLoadTimeout(downloadConfig.m3u8Timeout, TimeUnit.MILLISECONDS);
                    browser.get(url);
                } catch (Exception e) {
                    //一般是超时,由上面的代码抛出异常
                    continue;
                }
                String title = browser.getTitle();
                log.info("页面,加载成功={}", title);

                /*
                获取http日志
                .每次browser.get后,只能获取一次logEntries, (第二次获取时,最多只有几条没用的log)
                 */
                LogEntries logEntries = browser.manage().logs().get("performance");//耗时短
                List<LogEntry> logs = logEntries.getAll();
                log.info("logEntries={}", logs.size());
                if (cancelState.isCanceled()) return null;

                //抓取m3u8的url
                logs = clearOldLog(logs, url);
                if (appConfig.exportChromeLog) {
                    //不管是否找到
                    exportChromeLog(logs, task);
                }

                String m3u8Url = messageMatcher.getM3u8Url(logs);
                if (m3u8Url != null) {
                    log.info("找到url={}", m3u8Url);
                    return new HtmlInfo(title, m3u8Url);
                }
            }
        } finally {
            if (browser != null) browser.get("about:blank");//跳转到空白页,避免页面继续活动
            lock.unlock();
        }
    }

    private static final String[] urlKey = "message.params.request.url".split("\\.");

    /**
     * 过滤上个页面的日志
     *
     * @param htmlUrl 本次请求的网址
     */
    private List<LogEntry> clearOldLog(List<LogEntry> logs, String htmlUrl) {
        int size = logs.size();
        int htmlSendIndex = -1;//本页面,第一条发出的请求,访问的网址
        for (int i = 0; i < size; i++) {
            String message = logs.get(i).getMessage();
            if (!message.contains("Network.requestWillBeSent\"")) continue;

            JSONObject jsonObject = (JSONObject) JSON.parse(message);
            String url = CommonUtil.getJSONObjectString(jsonObject, urlKey);
            if (Objects.equals(url, htmlUrl)) {
                log.info("找到第一条请求,i={},  {}", i, htmlUrl);
                htmlSendIndex = i;
                break;
            }
        }
        if (htmlSendIndex == -1) throw new AppCommonException("找不到第一条请求: " + htmlUrl);

        //截取
        //logs.subList(htmlSendIndex, logs.size() - 1);//java基础: 不要用,内存泄露!
        ArrayList<LogEntry> res = new ArrayList<>(size - htmlSendIndex);
        for (int i = htmlSendIndex; i < size; i++) res.add(logs.get(i));
        return res;
    }

    private void exportChromeLog(List<LogEntry> logs, TaskInfo task) {
        String fileName = "chrome_" + DateBuilder.format(new Date(), "yyMMdd_HHmmss_SSS") + "_" + task.getShortName() + ".log ";
        try (FileWriter fw = new FileWriter(new File(LOG_HOME, fileName))) {
            for (LogEntry log : logs) {
                fw.write(log.getMessage());
                fw.write('\n');
            }
            log.info("导出chrome请求日志.成功={}", logs.size());
        } catch (IOException e) {
            log.error("导出chrome日志异常:", e);
        }
    }

    public void destroy() {
        try {
            lock.lock();
            if (browser != null) {// 获取browser时也是要在锁内的
                log.info("浏览器.正在关闭");
                try {
                    browser.quit();
                    browser = null;
                    log.info("浏览器.已关闭");
                } catch (Exception ignored) {
                    log.info("浏览器.已关闭.2");
                }
            }
        } finally {
            lock.unlock();
        }
    }

    /**
     * @return 能否应用已更新的配置字段[ChromeDriver路径]
     */
    public boolean canReloadConfig() {
        return browser == null && !inited;
    }

    @AllArgsConstructor
    public static class HtmlInfo {
        public String htmlTitle;
        public String m3u8Url;
    }

}
